using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace DejaVuTocReader
{
    
    public class CachedInputStream : InputStream
    {
        //~ Instance fields ------------------------------------------------------

        // Buffer used to access data
        public DataPool buffer=null;
        
        // Used for the mark and reset features.
        public int markOffset=0;

        // The current position of this stream in the data pool.
        private int offset=0;

        // The end position of this stream in the data pool.
        public int endOffset=0;


         // Index of the current data block
        private int blockIndex=-1;
        
        // The data block currently being read
        private byte[] block=null;

      

        // We can assign a name to this Stream
        private String name=null;

        //~ Constructors ---------------------------------------------------------

        /**
         * Creates a new CachedInputStream object.
         */
        public CachedInputStream()
        {
        }

        


        /**
         * Create an a new CachedInputStream.
         *
         * @param size the maximum amount of data to read from this stream
         *
         * @return the newly created CachedInputStream
         */
        public CachedInputStream createCachedInputStream(int size)
        {
          CachedInputStream retval=(CachedInputStream)this.clone();
          retval.setSize(size);
          return retval;
        }

        /**
         * Initialize the stream with a data source, startOffset, and endOffset.
         * 
         * @param buffer DataPool containing data.
         * @param startOffset Starting position of this stream.
         * @param endOffset Ending position of this stream.
         * 
         * @return the initialized stream
         */
        public CachedInputStream init(DataPool buffer,int startOffset,int endOffset)
        {
          this.buffer=buffer;
            markOffset = offset = startOffset;
          //buffer.Seek(startOffset,SeekOrigin.Begin);
          this.endOffset=endOffset;
          return this;
            
        }
        
        
        
        /**
         * Initialize the stream by copying the supplied input.
         *
         * @param input InputStream to copy
         *
         * @return the initialized stream
         */
        public CachedInputStream init(InputStream input)
        {
          
              if(input is CachedInputStream)
              {
                CachedInputStream i=(CachedInputStream)input;
                return init(i.buffer,i.offset,(int)i.getEndOffset());         
              }
              else
              {
                return init((new DataPool()).init(input),0, int.MaxValue);
              }
          
        }

        //public CachedInputStream init(BSInputStream input)
        //{
        //    return init(input, (int)input.zp.ibs.buffer.Position, int.MaxValue);
        //}
        
        /**
         * Create a copy of this stream which will referes to the same DataPool
         * 
         * @return the newly created copy
         */
        public Object clone()
        {

            return base.clone();
         
        }

        public void close()
        {
            
            endOffset = markOffset = 0;
        }

        /**
         * Query the end position.  This value may only be reduced, never increased.
         *
         * @return an offset past the last byte of this stream
         */
        public int getEndOffset()
        {
           
              int endOffset = buffer.getEndOffset();
              return (endOffset<this.endOffset)?endOffset:this.endOffset;
            
            
        }

        
        /**
         * Set the maximum number of bytes left in this stream.  
         * Size may only be reduced, never increased.
         *
         * @param size the new size
         */
        public void setSize(int size)
        {
          long endOffset=offset+size;
          if(endOffset < this.endOffset)
          {
            this.endOffset=(int)endOffset;
          }
        }
        
        /**
         * Query how much data is available.
         *
         * @return number of buffered bytes
         */
        public override int available()
        {
            int position = buffer.getCurrentSize();
          int retval=(position < endOffset)?position:endOffset;
          return (retval > 0)?retval:0;
        }

        /**
         * Marks the current location for a reset() later.
         *
         * @param readLimit ignored
         */
        public override void mark(int readLimit)
        {
          markOffset = offset;
        }

        /**
         * Query if mark is supported.
         *
         * @return true
         */
        public bool markSuppoted()
        {
          return true;
        }

        /**
         * Read the next byte of data ranged 0 to 255.  Returns -1 if an EOF has been read.
         *
         * @return next byte and -1 if an error occurs
         */
        public override int read()
        {
          int retval=-1;
          int index=offset/DataPool.BLOCKSIZE;
          if(index != blockIndex)
          {
            block=buffer.getBlock((int)index, true);
            blockIndex=index;
            if(block == null)
            {
              offset = (int)getEndOffset();
              blockIndex = -1;
            }
          }
          if(offset < getEndOffset())
          {
            retval=0xff&block[offset++%DataPool.BLOCKSIZE];
          }
          return retval;
        }

        /**
         * Read data into an array of bytes.
         *
         * @param b byte array to copy data to
         * @param off byte array offset to start copying to
         * @param len maximum number of bytes to copy
         *
         * @return number of bytes read and -1 if no bytes were read
         */
        public int read(byte[] b,int off,int len)
        {
          int retval=-1;
          int index=offset/DataPool.BLOCKSIZE;
          if(index != blockIndex)
          {
            block=buffer.getBlock(index, true);
            blockIndex=index;
            if(block == null)
            {
              offset = (int)getEndOffset();
              blockIndex = -1;
            }
          }
          if(offset < getEndOffset())
          {
            int offs=this.offset%DataPool.BLOCKSIZE;
            retval = DataPool.BLOCKSIZE-offs;
            if(retval > len)
            {
              retval = len;
            }
            Array.Copy(block, offs,b,off,retval);
            this.offset+=retval;
          }
          return retval; 
        }

        /**
         * Read data into an array of bytes.  The number of bytes
         * read is the array length unless an EOF is read.
         *
         * @param b byte array to copy data to
         *
         * @return number of bytes read
         */
        public override int read(byte [] b)
        {
          for(int remaining=b.Length;remaining > 0;)
          {
            int retval=b.Length-remaining;
            int len=read(b, retval, remaining);
            if(len < 0)
            {
              return (retval > 0)?retval+1:(-1);              
            }
            remaining-=len;
          }
          return b.Length;
        }

        /** 
         * Read the next two bytes as a posative integer.  If EOF is reached,
         * then return -1.
         *
         * @return the value read
         */
        public int read16()
        {
          int msb = read();

          if(msb < 0)
          {
            return msb;
          }

          

          int lsb = read();

          return (lsb >= 0)
          ? ((msb << 8) | lsb)
          : (-1);
        }

        /**
         * Read the next three bytes as a posative integer.  If EOF is reached,
         * then return -1.
         *
         * @return the value read
         */
        public int read24()
        {
          int msb = read16();

          if(msb < 0)
          {
            return msb;
          }

          int lsb = read();

          return (lsb >= 0)
          ? ((msb << 8) | lsb)
          : (-1);
        }

        /**
         * Restore the marked position
         *
         * @throws IOException if an error occurs
         */
        public void reset()
        {
            offset = markOffset;
        }

        /**
         * Skip bytes without reading.
         *
         * @param n number of bytes to skip
         *
         * @return number of bytes actually skipped
         */
        public override long skip(long n)
        {
          int endOffset=getEndOffset();
          long retval=((long)endOffset <= n+(long)offset)?(endOffset-offset):(int)n;
          if(retval > 0)
          {
            offset+=(int)retval;
          }
          else
          {
            retval=0;
          }
          return retval;
        }



            /**
       * Prefetch data in the same thread.
       */
        public void prefetchWait()
      {
        try
        {
          for(int i=offset/DataPool.BLOCKSIZE;i<getEndOffset()/DataPool.BLOCKSIZE;i++)
          {
            buffer.getBlock(i,true);
          }
          buffer.getBlock((getEndOffset()-1)/DataPool.BLOCKSIZE, true);
        }
        catch(Exception)
        {
            // ignored
        }
      }
            
      /**
       * Convert the accessable data into a string.  First a java modified UTF-8
       * translation will be tried.  If that fails, the data will be converted
       * in the current locale encoding.  Unlike ImageInputStream.readUTF
       * the first two bytes are not a length.
       *
       * @param textsize maximum amount of data to read
       *
       * @return the newly created string
       *
       * @throws IOException if an error occurs
       */
      public String readSizedUTF(int textsize)
      {
        //int current = buffer.Position;
        String retval = createCachedInputStream(textsize).readFullyUTF();
        skip(textsize);
        return retval;
      }
      
      /**
       * Convert the accessable data into a string.  First a java modified UTF-8
       * translation will be tried.  If that fails, the data will be converted
       * in the current locale encoding.  Unlike an ImageInputStream.readUTY
       * the first two bytes are not a length.
       *
       * @return the newly created string
       *
       * @throws IOException if an error occurs
       */
      public String readFullyUTF()
      {
        MemoryStream output=new MemoryStream();
        //output.WriteByte(0);
        //output.WriteByte(0);
        for(int i=read();i>=0;i=read())
        {
          output.WriteByte((byte)i);
        }

        output.Seek(0, SeekOrigin.Begin);
      StreamReader sr = new StreamReader(output,Encoding.UTF8);

          
          
      return sr.ReadToEnd();
      }
      
      /**
       * Query the name of this stream.
       *
       * @return the stream name
      
       * */
      public String getName()
      {
        return name;
      }
      
      /**
       * Set the name of this stream.
       *
       * @param name the stream name
       */
      public void setName(String name)
      {
        this.name=name;
      }
      
      /**
       * Test if the underlying file has a DjVu octet signature.
       *
       * @return true if this is a valid DjVu file.
       */
      public bool isDjVuFile()
      {
          byte[] b = buffer.getBlock(0, true);
        
        return (b != null)&&(b[0] == 65)&&(b[1] == 84)&&(b[2] == 38)&&(b[3] == 84);
      }

      /**
       * Query the enumeration of IFF chunks.  Returns null, if the name is 
       * four characters long.
       *
       * @return an Enumeration of CachedInputStream or null.
       */
      public IFFEnumeration getIFFChunks()
      {
        IFFEnumeration retval=null;
        if ((name == null)||(name.Length != 4))
        {
            retval = new IFFEnumeration();
          retval.init(this);
        }
        return retval;
      }


        

    }


}
